Skip to content

feat: toHaveScreenshot drop-in — route existing Playwright assertions through Percy - #637

Closed
Shivanshu-07 wants to merge 7 commits into
masterfrom
feat/tohavescreenshot-dropin
Closed

feat: toHaveScreenshot drop-in — route existing Playwright assertions through Percy#637
Shivanshu-07 wants to merge 7 commits into
masterfrom
feat/tohavescreenshot-dropin

Conversation

@Shivanshu-07

Copy link
Copy Markdown
Contributor

Ports the toHaveScreenshot drop-in (PER-8985) into this SDK so it ships from @percy/playwright instead of a separate package. One require line converts a repo's existing Playwright screenshot assertions into Percy visual tests — no test changes, no rewrites.

// playwright.config.js
require('@percy/playwright/dropin');
PERCY_TOKEN=<token> npx percy-playwright exec -- npx playwright test

What's in the box (dropin/)

  • The override (dropin/index.js): replaces Playwright's built-in toHaveScreenshot matcher on the shared expect. The public expect.extend() silently ignores built-in matcher names on the shared instance (the override only exists on the discarded return value), so registration writes into expect's internal userMatchers — which call-time dispatch spreads after built-ins. On an unrecognized internal shape it degrades to extend() with a loud warning, never a silent no-op. Verified on @playwright/test 1.60/1.61.
  • Never fail the suite: any Percy error is swallowed (warn/debug); if Percy is down at run start the whole run delegates to the genuine native matcher (pristine-expect chain snapshotted before the override), so consumers keep real pixel-diff protection.
  • Identity (dropin/identity.js): snapshot names reproduce Playwright's on-disk baseline stems byte-for-byte (same sanitizer regex, same per-test anonymous counters) so uploads pair with committed baselines.
  • Capture modes (.percy-playwright-dropin.json):
    • screenshot (default): stabilized PNG → raw-image comparison (generic/app projects); tag width = identity width, tag height parsed from PNG bytes (percy-api validates height presence).
    • snapshot: serialized-DOM web snapshot for web projects — delegates to this repo's own captureDOM, inheriting the readiness gate, responsive capture, and cross-origin iframe handling; the two entry points cannot drift. Locator subjects become element-scoped snapshots (transient data-percy-dropin-scope marker + scope).
  • First-build-as-baseline: the percy-playwright wrapper bin sets PERCY_DROPIN_BASELINE_CANDIDATE=true (mirrors PERCY_BUILD_SOURCE); percy-api decides first-ness server-side and, on a genuine first build, dropin/global-setup.js seeds the repo's committed baseline PNGs as the build's content (8-way parallel) while the override skips live captures. The server auto-approves that baseline (org-flag-gated) — diffs start on run 2. Server side lands via percy/percy-api#6178; CLI plumbing via feat(client/sdk-utils): additive support for @percy/playwright-dropin baseline seed + sync verdict cli#2275.
  • Opt-in extras: dropin/reporter.js (fail-on-changes CI gate, first build always review-only) and sync-assertion mode (inline verdicts, .percy.yml snapshot.sync, read-token guarded).

Repo-level changes

  • index.js: exports captureDOM for internal reuse; ENV_INFO's top-level require('playwright/package.json') crashed at import time under the 1.61 runner (CJS↔ESM interop) — now guarded with graceful fallbacks.
  • package.json: ships dropin/ + bin/, adds the percy-playwright bin, @playwright/test as an optional peer (only the dropin entry needs it), devDeps bumped ^1.24.2 → ^1.60.0 (the drop-in requires modern expect internals; existing suite passes unchanged on 1.61).
  • Types + README section.

Testing

  • 117 passed (full suite on @playwright/test 1.61.1), lint clean.
  • New dispatch specs drive the REAL Playwright expect through the override and hard-assert the posted comparison (name/tag dims/tile) — the exact check that prevents a silent registration no-op; wrapped in expect().toPass() because the shared CLI testing server is reset concurrently by parallel workers.
  • Unit specs pin name derivation, first-build seeding (server-decision keyed, PNG-dims tags), and locator scope-marker lifecycle.
  • The same implementation was E2E-validated against staging percy-api (first build seeded + auto-approved flag-on; run-2 diffs against it) in PER-8985.

Notes for reviewers

  • The registration and native-fallback lean on Playwright expect internals (META_INFO symbol / userMatchers); the degrade path is loud by design. This is the part most worth scrutiny.
  • dropin/ is a direct port of percy/percy-playwright-dropin PR #2 minus its standalone DOM capture (replaced by captureDOM reuse). If this lands, the standalone package can be retired before ever publishing.

🤖 Generated with Claude Code

…sertions through Percy

Adds @percy/playwright/dropin: one require line in playwright.config.js
overrides Playwright's toHaveScreenshot() so every existing screenshot
assertion is captured and uploaded to Percy, with the suite never failing on
a Percy problem (missing token / CLI down → the whole run transparently falls
back to native toHaveScreenshot).

Registration writes the matcher into Playwright expect's internal userMatchers
(call-time dispatch spreads them after built-ins) because the public
expect.extend() silently ignores built-in matcher names on the shared
instance; on an unrecognized expect shape it degrades to extend() WITH a loud
warning — never a silent no-op. Verified working on @playwright/test 1.60/1.61.

Capture modes (config file .percy-playwright-dropin.json):
- screenshot (default): the Playwright-stabilized PNG is uploaded as a raw
  comparison (generic/app projects); snapshot identity (name/browser/width)
  mirrors Playwright's on-disk baseline naming byte-for-byte, and tag
  dimensions are parsed from the PNG (percy-api validates height).
- snapshot: serialized-DOM web snapshot for web projects — DELEGATES to this
  repo's own captureDOM, so the drop-in inherits the readiness gate,
  responsive capture and cross-origin iframe handling and cannot drift from
  percySnapshot(). Locator subjects become element-scoped snapshots via a
  transient data attribute + scope.

First-build-as-baseline: the bundled percy-playwright wrapper bin flags the
run's build as a baseline candidate (PERCY_DROPIN_BASELINE_CANDIDATE, mirrors
PERCY_BUILD_SOURCE); percy-api decides first-ness server-side, and on a
genuine first build the dropin globalSetup seeds the repo's committed
Playwright baseline PNGs as the build's content (8-way parallel) while the
override skips live captures — the server auto-approves it (flag-gated), so
diffs start on the very next run. Also ships the opt-in fail-on-changes gate
reporter and sync-assertion mode.

Compat fixes surfaced by the @playwright/test 1.24→1.60 devDep bump:
- ENV_INFO's top-level require('playwright/package.json') crashes at import
  time under the 1.61 runner (CJS↔ESM interop) — now guarded, degrades to
  @playwright/test's version or a bare label
- the dropin↔root require is lazy for the same interop reason

Tests: dispatch specs drive the REAL Playwright expect through the override
and hard-assert the posted comparison (retry-wrapped against the shared
testing-server reset race); unit specs pin name derivation, first-build
seeding and locator scoping. 117 passed, lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07 requested a review from a team as a code owner July 8, 2026 16:14
Comment thread dropin/config.js Fixed
Shivanshu-07 and others added 6 commits July 8, 2026 21:45
The toHaveScreenshot drop-in needs modern @playwright/test (expect internals),
whose engine floor is Node 18. Runtime support for the core SDK entry is
unchanged; this bumps the dev/CI toolchain only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playwright >=1.39 no longer downloads browsers as a package postinstall; the
suite failed on CI with 'Executable doesn't exist'. Fetch the launched
browser (with system deps) after yarn install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ope nyc to the core surface

nyc instruments the arrow functions dropin/dom.js ships to the browser via
locator.evaluate, and the injected coverage counters don't exist in the page
(ReferenceError: cov_… is not defined) — annotate them with istanbul ignore,
the same convention index.js/utils.js already use for browser-executed code.

Exclude dropin/ and bin/ from the 100% coverage gate for now: the modules are
covered by the standalone package's 154-test unit suite, and porting that
suite into this harness is a flagged fast-follow. The existing core surface
keeps its 100% bar unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Which fallback executes depends on the consumer's installed packages — the
defensive branches can't all be exercised in one environment and were the
last gap under the 100% coverage gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node 18 went EOL in April 2025; its leg also mis-collects subprocess coverage
for utils.js under the percy-exec harness (identical 117-test pass count,
different coverage) — not worth chasing on a dead runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves the 7 blocking semgrep findings (path-join-resolve-traversal,
CWE-22): NUL bytes are stripped from any externally supplied directory
before it reaches path.join, and directory-walk entry names must be a
single path component (no separators, no dot/dot-dot) or the entry is
skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants